package org.diy.traffic.sources;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.diy.traffic.updates.TrafficUpdate;
import org.diy.traffic.util.TLog;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * This class represents a traffic source based on an XML feed format. It cannot be instantiated,
 * but must be subclassed. This class handles all the network communications, and has convenience
 * functions for parsing out DOM nodes, and then fetching particular elements of those nodes.
 * @author Dan Greenblatt
 *
 */
public abstract class FeedSource extends TrafficSource {
	
	private XPath xPath;
	private String requestBase = null;
	
	/**
	 * Constructor for FeedSource uses the default source update frequency
	 * @param unique_name A unique String value to identify this FeedSource
	 * @param base_feed_url The url of the feed from which to fetch XML data
	 */
	public FeedSource(String unique_name, String base_feed_url) {
		// this constructor does not take a update frequency, so use the default
		this(unique_name, base_feed_url, TrafficSource.DEFAULT_UPDATE_FREQUENCY_S); //900 seconds is 15 minutes;
	}
	
	/**
	 * Base constructor for FeedSource
	 * @param unique_name A unique String value to identify this FeedSource
	 * @param base_feed_url The URL of the feed from which to fetch XML data
	 * @param update_frequency A interval in seconds on which this source should be updated
	 */
	public FeedSource(String unique_name, String base_feed_url, int update_frequency) {
		// this constructor does not take a update frequency, so use the default
		super(unique_name, update_frequency); //900 seconds is 15 minutes;
	
		this.requestBase = base_feed_url;
		
		// A bunch of XML parsing....
        XPathFactory factory = XPathFactory.newInstance();
        xPath=factory.newXPath();
	}
	

	/**
	 * Fetches a feed, referenced by this object's requestBase + any supplied parameters, as an InputStream
	 * @param parameterMap Additional parameters to add to requestBase
	 * @return An InputStream for requestBase + parameters
	 * @throws FeedSourceException If an error occurs during network communications
	 */
	protected InputStream getFeedAsStream(HashMap<String, String> parameterMap) throws FeedSourceException {
		// will contain the TrafficUpdate objects, if there are any
		ArrayList<TrafficUpdate> trafficUpdates = new ArrayList<TrafficUpdate>();
		
		// build the HTTP request
		String request = requestBase;
		
		// add in any additional keys which were put in the parameterMap
		if (parameterMap != null) {
			Set<String> keys = parameterMap.keySet();
			Iterator<String> key_iterator = keys.iterator();
			while (key_iterator.hasNext()) {
				String next_key = key_iterator.next();
				String next_val = parameterMap.get(next_key);
				request += ( "&" + next_key + "=" + plusEncode(next_val));			
			}
		}
		

		
        HttpClient client = new HttpClient();
        
		// can i put a proxy in here for now (for testing?)
        //client.getHostConfiguration().setProxy("wwwgate0.mot.com", 1080);
        
        GetMethod method = new GetMethod(request);
        
        //System.err.println("***REQUEST HERE IS: " + request);
        
        // Send GET request
        int statusCode;
        try {
        	statusCode = client.executeMethod(method);
        } catch (HttpException he) {
        	throw new FeedSourceException("Failure while executing \"GET\": " + he.getMessage());
        } catch (IOException ioe) {
        	throw new FeedSourceException("Failure while executing \"GET\": " + ioe.getMessage());
        }
        
        if (statusCode != HttpStatus.SC_OK) {
        	// if something has gone wrong ....
        	//TLog.log(Level.SEVERE, "Method failed: " + method.getStatusLine());
        	FeedSourceException fse = new FeedSourceException(
        			"FeedSource got server error (" + method.getStatusLine() +")",
        			statusCode);
        	throw fse;
        }
        
        InputStream response_stream = null;
        
        // Get the response body
        try {
        	response_stream = method.getResponseBodyAsStream();
        } catch (IOException ioe) {
        	throw new FeedSourceException("Error getting \"GET\" response body: " + ioe.getMessage());
        }
        
        return response_stream;
	}
	
	/**
	 * Get a list of XML Nodes from an InputStream to an XML feed resource on the web
	 * @param responseStream An InputStream to an XML feed on the web
	 * @param nodePath A String representing the path to the node to fetch, i.e. "/channel/item"
	 * @return A list of Xpath nodes, where each node represents one instance of an XML node represented by nodePath
	 * @throws FeedSourceException If an error occurs during XML parsing
	 */
	protected NodeList getNodes(InputStream responseStream, String nodePath) throws FeedSourceException {
		// Process response
        Document response;
        try {
        	response = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(responseStream);
        } catch (Exception e){
        	throw new FeedSourceException("Exception [" + e.getClass()+ "] while building XML doc: " + e.getMessage());
        } 
          
        //Get all search Result nodes
        
        NodeList nodes;
        try {
        	// Get each <ResultSet><Result> node
        	nodes = (NodeList)xPath.evaluate(nodePath, response, XPathConstants.NODESET);
        } catch (XPathExpressionException xpee)  {
        	throw new FeedSourceException(String.format("Error parsing node path \"%s\": %s", nodePath, xpee.getMessage()));
        }	
	
        return nodes;
	}
	
	/**
	 * Get the value of one or multiple elements from each XMLPath Node within a NodeList
	 * @param nodes A list of xPath nodes.
	 * @param elements An array of strings, where each String represents an element within the Node
	 * @return An ArrayList of HashMaps, where each item in the List contains a HashMap who's key is an element specified in 'elements' and who's value
	 * is the value of that element in the XML node
	 * @throws FeedSourceException If an error occurs during XML parsing.
	 */
	protected ArrayList<HashMap<String,String>> getElementsFromNodes(NodeList nodes, String[] elements) throws FeedSourceException {
		ArrayList<HashMap<String, String>> allNodes = new ArrayList<HashMap<String, String>>();
		
		//Iterate over search Result nodes 
        int nodeCount = nodes.getLength();
        for (int i = 0; i < nodeCount; i++) {
           
        	//Get each xpath expression as a string
        	HashMap<String, String> nodeContent = new HashMap<String, String>();
        	for (int c=0; c<elements.length; c++) {
        		String node_name = elements[c];
        	
        		try {
        			nodeContent.put(node_name,
        					(String)xPath.evaluate(node_name, nodes.item(i), XPathConstants.STRING));
        		}  catch (XPathExpressionException xpee) {
            		throw new FeedSourceException(String.format("Can't find element \"%s\" in feed: %s", node_name, xpee.getMessage()));
        		} // end catch
        	} // end inner for loop
        	
        	allNodes.add(nodeContent);
         } // end outer for loop
        
        return allNodes;
	}
	

	/**
	 * TODO IS THIS CORRECT?
	 * Encodes a String to be safe for use in a URL. Substitutes "+" in place of " "
	 * @param val a string to be plus-encoded
	 * @return a string which represents val, with all " " characters replaced by "+" characters
	 */
	private String plusEncode(String val){
		return val.replace(" ", "+");
	}	
	
	/**
	 * Helper function to print out a list of TrafficUpdates
	 * @param l the list of TrafficUpdates to print.
	 */
	private void printUpdates(ArrayList<TrafficUpdate> l) {
		Iterator<TrafficUpdate> i = l.iterator();
		while (i.hasNext()) {
			TLog.log(Level.FINEST, (i.next()).toString());
		}
	}
	
	public class FeedSourceException extends TrafficSourceException {
		
		public int statusCode = -1;
		
		public FeedSourceException() {super();}
		
		public FeedSourceException(Exception e) {super(e);}
		
		public FeedSourceException(String s) {super(s);}
		
		public FeedSourceException(String s, int code) {
			super(s);
			this.statusCode = code;
		}
	}
}
